package edu.upol.paradise.compiler;

import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.Stack;
import java.util.UUID;

import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtConstructor;
import javassist.CtField;
import javassist.CtMethod;
import javassist.CtNewConstructor;
import javassist.CtNewMethod;
import javassist.bytecode.AttributeInfo;
import javassist.bytecode.Bytecode;
import javassist.bytecode.CodeAttribute;
import javassist.bytecode.ConstPool;
import javassist.bytecode.InstructionPrinter;
import javassist.bytecode.LineNumberAttribute;
import javassist.bytecode.Opcode;
import javassist.bytecode.SourceFileAttribute;
import javassist.compiler.CompileError;

import org.apache.log4j.Logger;

import edu.upol.paradise.runtime.ParadiseRuntime;
import edu.upol.paradise.utils.ElementMetadata;
import edu.upol.paradise.utils.IndexedSet;
import edu.upol.paradise.utils.Utils;
import edu.upol.paradise.utils.VariableClosureStack;
import edu.upol.paradise.compiler.ParadiseCompilerSettings.OptimizationLevel;
import edu.upol.paradise.compiler.types.AnnotatedCons;
import edu.upol.paradise.compiler.types.Cons;
import edu.upol.paradise.compiler.types.Cons.PPairIterator;
import edu.upol.paradise.compiler.types.Nil;
import edu.upol.paradise.compiler.types.ParadiseObject;
import edu.upol.paradise.compiler.types.ParadisePair;
import edu.upol.paradise.compiler.types.ParadiseString;
import edu.upol.paradise.compiler.types.ParadiseSymbol;

public class ParadiseCompiler {
	private static final String STANDARD_PARADISE_JVM_COMPILER_VERSION = "0.0.1 build 000";
	static final Logger log = Logger.getLogger(ParadiseCompiler.class);
	
	private ClassPool cp;
	private ParadiseCompilerSettings settings;
	
	public ParadiseCompiler(ParadiseCompilerSettings settings){
		log.debug("Initializing new Paradise Compiler, version " + getVersion() + ", Settings: " + settings);
		cp = ClassPool.getDefault();
		this.settings = settings;
		log.debug("ParadiseCompiler classpool: " + cp);
	}

	private String getVersion() {
		return STANDARD_PARADISE_JVM_COMPILER_VERSION;
	}
	
	private String source;
	private ParadiseRuntime runtime;
	private VariableClosureStack closureStack;

	public CompiledBlockMetadescriptor compileSource(ParadiseRuntime paradiseRuntime,
			List<ParadiseObject> astTree, String source, String blockName) throws Exception {
		this.source = source;
		this.runtime = paradiseRuntime;
		closureStack = new VariableClosureStack();
		
		closureStack.push();
		for (ParadiseObject item : astTree)
			markVariables(item);
		closureStack.pop(null);
		
		List<ParadiseObject> newTree = new ArrayList<ParadiseObject>();
		for (ParadiseObject expression : astTree)
			newTree.add(findAndCompileFunctionBlocks(expression, blockName));
		
		ParadiseObject collectedPair = Utils.asPair(newTree);
		
		if (source.contains("."))
			source = source.split("\\.")[0];
		return compileBlock(collectedPair, source + blockName);
	}
	
	private void markVariables(ParadiseObject item) throws CompilerError {
		if (item.isAtom() && item instanceof ParadiseSymbol){
			closureStack.addSymbol(((ParadiseSymbol)item).toString());
		} else if (!item.isAtom()){
			if (item != Nil.NIL){
				ParadisePair ppair = (ParadisePair)item;
				ParadiseObject head = ppair.car();
				if (Keywords.LAMBDA.equals(head.toString())){
					List<ElementMetadata> elist = Utils.annotatedPair2List(ppair);
					if (elist.size() == 1)
						throw new edu.upol.paradise.compiler.CompilerError("Incorrect lambda list at " + elist.get(1).lineNo);
					if (elist.size() > 2){
						closureStack.push();
						closureStack.addHeader(elist.get(1).element);
						for (int i=2; i<elist.size(); i++){
							markVariables(elist.get(i).element);
						}
						closureStack.pop(ppair);
					} else if (Keywords.QUOTE.equals(head.toString())){
						return;
					} else {
						for (ElementMetadata mdata : elist)
							markVariables(mdata.element);
					}
				}
			}
		}
	}

	@SuppressWarnings("unused")
	private boolean optimize() {
		return settings.getOptiLevel() == OptimizationLevel.SAFETY_OFF;
	}

	private CompiledBlockMetadescriptor compileBlock(ParadiseObject astTree, String blockName) throws Exception {
		Class<?> block = compile(astTree, blockName);
		
		CompiledBlockMetadescriptor meta = new CompiledBlockMetadescriptor(block, astTree);
		return meta;
	}
	
	private ParadiseObject findAndCompileFunctionBlocks(ParadiseObject expression, String blockName) throws Exception{
		if (expression instanceof ParadisePair && expression != Nil.NIL){
			List<ElementMetadata> argList = Utils.annotatedPair2List((ParadisePair) expression);
			if (argList.get(0).element instanceof ParadiseSymbol){
				ParadiseSymbol head = (ParadiseSymbol)argList.get(0).element;
				if (head.toString().equals(Keywords.LAMBDA)){
					if (argList.size() < 2)
						throw new CompilerError("Invalid lambda list, minimum 2 elements required at " + argList.get(0).lineNo);
					ParadiseObject rest = Nil.NIL;
					String uid = UUID.randomUUID().toString();
					if (argList.size() > 2){
						rest = Cons.cddr((ParadisePair)expression);
						findAndCompileFunctionBlocks(rest, blockName);
					}
					CompiledBlockMetadescriptor block = compileBlock(rest, "lambda-"+uid);
					runtime.addCompiledFunction(block, uid);
					List<ParadiseObject> retList = new ArrayList<ParadiseObject>();
					retList.add(new ParadiseSymbol("#:LOAD-COMPILED-FUNCTION"));
					retList.add(new ParadiseString(uid));
					
					List<ParadiseObject> qaList = new ArrayList<ParadiseObject>();
					qaList.add(new ParadiseSymbol("QUOTE"));
					qaList.add(argList.get(1).element);
					AnnotatedCons qapair = (AnnotatedCons) Utils.asPair(qaList, true);
					ParadisePair qahead = qapair;
					while (qahead != Nil.NIL){
						if (expression instanceof AnnotatedCons)
							((AnnotatedCons)qahead).setMetainfo(((AnnotatedCons)expression).getLineNo(),
									((AnnotatedCons)expression).getCharNo());
						qahead = (ParadisePair) qahead.cdr();
					}
					retList.add(qapair);
					
					Set<String> sset = closureStack.get(expression);
					if (sset != null){
						for (String s : sset){
							List<ParadiseObject> pList = new ArrayList<ParadiseObject>();
							pList.add(new ParadiseSymbol("QUOTE"));
							pList.add(new ParadiseSymbol(s));
							AnnotatedCons pair = (AnnotatedCons) Utils.asPair(pList, true);
							ParadisePair phead = pair;
							while (phead != Nil.NIL){
								if (expression instanceof AnnotatedCons)
									((AnnotatedCons)phead).setMetainfo(((AnnotatedCons)expression).getLineNo(),
											((AnnotatedCons)expression).getCharNo());
								phead = (ParadisePair) phead.cdr();
							}
							retList.add(pair);
							retList.add(new ParadiseSymbol(s));
						}
					}
					AnnotatedCons ret = (AnnotatedCons) Utils.asPair(retList, true);
					ParadisePair rhead = ret;
					while (rhead != Nil.NIL){
						if (expression instanceof AnnotatedCons)
							((AnnotatedCons)rhead).setMetainfo(((AnnotatedCons)expression).getLineNo(),
									((AnnotatedCons)expression).getCharNo());
						rhead = (ParadisePair) rhead.cdr();
					}
					
					return ret;
				} else if (head.toString().equals(Keywords.QUOTE)){
					return expression;
				} else {
					PPairIterator it = Cons.makePPairIterator(((ParadisePair) expression));
					while (it.hasNext()){
						it.set(findAndCompileFunctionBlocks(it.next(), blockName));
					}	
				}
			} else {
				PPairIterator it = Cons.makePPairIterator(((ParadisePair) expression));
				while (it.hasNext()){
					it.set(findAndCompileFunctionBlocks(it.next(), blockName));
				}
			}
		}
		return expression;
	}
	
	private int maxLocals;
	private IndexedSet<ParadiseObject> constants;
	private DataOutputStream lineNumberStream;
	private CtClass cls;
	private Bytecode bc;
	private ConstPool pool;
	private AutoIntStacker stacker;
	private int lastLineWritten;
	private Stack<Integer> lineNo;
	private int st; // stack size marker, used to make safe calls to mark
	private int variableInitLabel;
	private HashMap<String, Integer> cache;
	private Set<String> usedSymbols;

	@SuppressWarnings("unchecked")
	private Class<?> compile(ParadiseObject astTree, String blockName) throws Exception {
		
		st = 0;
		lastLineWritten = -1;
		maxLocals = 2;
		stacker = new AutoIntStacker(maxLocals);
		lineNo = new Stack<Integer>();
		usedSymbols = new HashSet<String>();
		cache = new HashMap<String, Integer>();
		constants = new IndexedSet<ParadiseObject>(new IndexedSet.Equalator<ParadiseObject>(){

			public boolean equal(ParadiseObject th, Object o) {
				return th.valueEquals((ParadiseObject) o);
			}

			public int hashCode(ParadiseObject x) {
				return x.atomHashId();
			}
			
		});
		
		if (blockName == null)
			blockName = Utils.genName();
		cls = cp.makeClass(blockName);
		cls.addInterface(cp.getCtClass(CompilerConstants.PARADISE_COMPILED_BLOCK));
		cls.setSuperclass(cp.getCtClass(CompilerConstants.PARADISE_COMPILED_BLOCK_STUB));
		
		CtField f = new CtField(cp.get("edu.upol.paradise.compiler.types.ParadiseObject[]"),
				"constants", cls);
		f.setModifiers(Modifier.STATIC | Modifier.PUBLIC);
		cls.addField(f);
		
		CtMethod mget = CtNewMethod.make("public edu.upol.paradise.compiler.types.ParadiseObject[] getConstants(){ return constants; }", cls);
		cls.addMethod(mget);
		
		CtMethod m = CtNewMethod.make("protected edu.upol.paradise.compiler.types.ParadiseObject runBytecode() { return null; }", cls);
		pool = m.getMethodInfo().getConstPool();
		ByteArrayOutputStream stream = new ByteArrayOutputStream();
		lineNumberStream = new DataOutputStream(stream);
		lineNumberStream.writeShort(0);
		
		SourceFileAttribute attr = new SourceFileAttribute(pool, source);
		cls.getClassFile().addAttribute(attr);
		
		bc = new Bytecode(pool);
		
		bc.add(Opcode.GOTO_W);
		variableInitLabel = bc.currentPc();
		bc.add(0, 0, 0, 0);
		
		if (astTree.isAtom() || astTree == Nil.NIL){
			markLine(bc.currentPc(), 0);
			compileAtom(astTree, true, 0);
		} else {
			List<ElementMetadata> list = Utils.annotatedPair2List((ParadisePair) astTree);
			for (int i=0; i<list.size(); i++){
				boolean last = i == list.size() - 1;
				compileExpression(list.get(i).element, last, list.get(i).lineNo);
			}
		}
		
		bc.add(Opcode.ARETURN);
		
		addLocalsSavingHandler();
		
		byte[] bytes = stream.toByteArray();
		int size = (bytes.length - 2) / 4;
		bytes[0] = (byte) ((size >>> 8) & 0xFF);
		bytes[1] = (byte) ((size >>> 0) & 0xFF);
		AttributeInfo lineNubmerInfo = new AttributeInfo(pool, LineNumberAttribute.tag, bytes);
		
		CodeAttribute at = bc.toCodeAttribute();
		at.computeMaxStack();
		//at.setMaxStack(128); // FIXME
		at.setMaxLocals(stacker.getMax());
		at.getAttributes().add(lineNubmerInfo);
		
		m.getMethodInfo().setCodeAttribute(at);
		
		log.debug("Compiled block of code into jvm: ");
		InstructionPrinter.print(m, System.err);
		
		cls.addMethod(m);
		
		CtConstructor ct = CtNewConstructor.defaultConstructor(cls);
		cls.addConstructor(ct);
		
		if (settings.isDebug())
			cls.debugWriteFile();
		
		Class<?> clazz = cls.toClass(runtime.getClassLoader(), null);
		clazz.getField("constants").set(null, constants.toIndexedArray(new ParadiseObject[]{}));
		return clazz;
	}
	
	private void addLocalsSavingHandler() throws Exception {
		int eoc = bc.currentPc();
		bc.write32bit(variableInitLabel, bc.currentPc()-variableInitLabel+1);
		bc.add(Opcode.NOP);
		for (int i=1; i<stacker.getMax(); i++){
			bc.add(Opcode.ACONST_NULL);
			bc.addAstore(i);
		}
		bc.add(Opcode.GOTO_W);
		bc.add32bit(-(bc.currentPc()-1)+5);
		
		bc.addExceptionHandler(5, eoc-1, bc.currentPc(), cp.get("java.lang.Throwable"));
		
		bc.addAload(0); // this
		bc.addAnewarray(cp.get("java.lang.Object"), stacker.getMax());
		for (int i=0; i<stacker.getMax(); i++){
			bc.add(Opcode.DUP);
			bc.addIconst(i);
			switch (i){
			case 0: // this
				bc.addAload(i);
				bc.addCheckcast(cp.get("java.lang.Object"));
				break;
			case 1: // last expression evaluated
				bc.addAload(i);
				bc.addCheckcast(cp.get("java.lang.Object"));
				break;
			default: // Function argument evaluated
				bc.addAload(i);
				bc.addCheckcast(cp.get("java.lang.Object"));
				break;
			}
			bc.add(Opcode.AASTORE);
		}
		bc.addPutfield("edu/upol/paradise/compiler/ParadiseCompiledBlockStub", "currentLocals", "[Ljava/lang/Object;");
		bc.add(Opcode.ATHROW);
	}

	private void compileExpression(ParadiseObject element, boolean last,
			Integer lineNo) throws Exception {
		if (element instanceof ParadisePair)
			compileListExpression((ParadisePair) element, last);
		else
			compileAtom(element, last, lineNo);
	}

	private void compileListExpression(ParadisePair paradisePair, boolean last) throws Exception{
		
		if (paradisePair == Nil.NIL){
			bc.addGetstatic(CompilerConstants.NIL, "NIL", CompilerConstants.TYPE_NIL);++st;
			return;
		}
		
		ParadisePair pair = paradisePair;
		if (!pair.isList()){
			if (pair instanceof AnnotatedCons)
				throw new CompilerError(CompilerError.ERROR_NOT_A_LIST, (AnnotatedCons) pair);
			else
				throw new CompilerError(CompilerError.ERROR_NOT_A_LIST);
		}
		ParadiseObject head = pair.car();
		if (!(head instanceof ParadiseSymbol)){
			if (head instanceof ParadisePair){
				// NOTHING
			} else if (pair instanceof AnnotatedCons)
				throw new CompilerError(CompilerError.ERROR_FIRST_ELEMENT_WRONG, (AnnotatedCons) pair);
			else
				throw new CompilerError(CompilerError.ERROR_FIRST_ELEMENT_WRONG);
		}
		
		int lineno = 0;
		
		if (pair instanceof AnnotatedCons){
			AnnotatedCons ppair = (AnnotatedCons)pair;
			lineno = ppair.getLineNo();
			lineNo.push(lineno); 
			markLine(bc.currentPc(), lineNo.peek());
		} else {
			lineNo.push(0); 
			markLine(bc.currentPc(), lineNo.peek());
		}
		
		ParadiseSymbol sym = null;
		if (head instanceof ParadiseSymbol)
			sym = (ParadiseSymbol)head;
		if (sym != null && Keywords.KEYWORD_SET.contains(sym.toString())){
			// specific keyword
			compileKeywords(sym.toString(), (ParadisePair) pair.cdr(), last, lineno);
		} else {
			int localVariableStore = stacker.acquire();
			
			List<ElementMetadata> args = Utils.annotatedPair2List((ParadisePair) pair.cdr());
			
			bc.addAnewarray(cp.get("edu.upol.paradise.compiler.types.ParadiseObject"), args.size()); st++;
			bc.addAstore(localVariableStore); st--;
			
			for (int i=0; i<args.size(); i++){
				ElementMetadata md = args.get(i);
				 
				bc.addAload(localVariableStore); st++; // load our array
				bc.addIconst(i); st++;
				compileExpression(md.element, true, md.lineNo);
				bc.addOpcode(Opcode.AASTORE); st--;st--;st--;
			}
			
			markLine(bc.currentPc(), lineNo.peek());
			bc.addAload(0); st++; 												// load this
			bc.addGetfield(cls, "runtime", "Ledu/upol/paradise/runtime/ParadiseRuntime;");
			compileExpression(head, true, lineno);
			bc.addCheckcast("edu/upol/paradise/compiler/types/ParadiseFunctor");
			markLine(bc.currentPc(), lineNo.peek());
			bc.addAload(localVariableStore); st++;								// load our array 
			bc.addInvokevirtual("edu/upol/paradise/runtime/ParadiseRuntime", "runFunctor", 
					"(Ledu/upol/paradise/compiler/types/ParadiseFunctor;[Ledu/upol/paradise/compiler/types/ParadiseObject;)Ledu/upol/paradise/compiler/types/ParadiseObject;");
			st -= 2;
			stacker.release();
		}
		
		// If expression is in block, if it is not last, set last returned value to null on stack
		if (!last){
			bc.add(Opcode.POP); --st;
			bc.add(Opcode.ACONST_NULL); ++st;
		}
		
		// Store last returned value from expression or null in locals
		bc.addAstore(1); --st;															
		
		if (st == 0){
			// We need to call markExecution with next bytecode after markExecution
			bc.addAload(0);														// load this
			bc.add(Opcode.LDC_W);												// Load int from attributes
			int mark = bc.currentPc();
			bc.add(0, 0);	
			bc.addAload(1);														// Load variable 1 from locals
			bc.addInvokevirtual("edu/upol/paradise/compiler/ParadiseCompiledBlockStub", "markExecutionPoint", "(ILedu/upol/paradise/compiler/types/ParadiseObject;)V");
			int value = bc.currentPc();
			bc.write16bit(mark, pool.addIntegerInfo(value));
		}
		
		if (last){
			bc.addAload(1);		++st;											// Load variable 1 from locals
		}
		
		lineNo.pop();																
	}

	private void compileKeywords(String kw, ParadisePair rest, boolean last, int lineNo) throws Exception {
		if (Keywords.PRINT.equals(kw))
			compilePrint(rest, "print", last);
		if (Keywords.PRINTLN.equals(kw))
			compilePrint(rest, "println", last);
		if (Keywords.PROGN.equals(kw))
			compileProgn(rest, last);
		if (Keywords.QUOTE.equals(kw))
			compileQuote(rest, last);
		if (Keywords.IF.equals(kw))
			compileIf(rest, last, lineNo);
		if (Keywords.DEFINE.equals(kw))
			compileDefine(rest, last, lineNo);
		if (Keywords.EQ.equals(kw))
			compileEQ(rest, last, lineNo);
		if (Keywords.EQUALS.equals(kw))
			compileEquals(rest, last, lineNo);	
		if (Keywords.ERROR.equals(kw))
			compileError(rest, last, lineNo);
		if (Keywords.SET.equals(kw))
			compileSet(rest, last, lineNo);
		if (Keywords.WHILE.equals(kw))
			compileWhile(rest, last, lineNo);
	}
	
	private void compileWhile(ParadisePair rest, boolean last, int lineNo2) throws Exception {
		List<ElementMetadata> argList = Utils.annotatedPair2List(rest);
		
		if (argList.size() < 2)
			throw new CompileError("While requires at least two arguments, got " + argList.size());
		
		bc.add(Opcode.NOP);
		int jmpBackArray = bc.currentPc();
		
		bc.addGetstatic(CompilerConstants.NIL, "NIL", CompilerConstants.TYPE_NIL);++st;
		compileExpression(argList.get(0).element, true, argList.get(0).lineNo);
		bc.add(Opcode.IF_ACMPEQ); --st;--st;
		int branchbyteCode = bc.currentPc();
		bc.add(0, 0);
		
		for (int i=1; i<argList.size(); i++)
			compileExpression(argList.get(i).element, false, argList.get(i).lineNo);
		
		bc.add(Opcode.GOTO_W);
		bc.add32bit(-(bc.currentPc()-jmpBackArray-1));
		
		bc.write16bit(branchbyteCode, bc.currentPc()-branchbyteCode+1);
		bc.add(Opcode.NOP);
		
		bc.addGetstatic(CompilerConstants.NIL, "NIL", CompilerConstants.TYPE_NIL);	++st;
	}

	private void compileSet(ParadisePair rest, boolean last, int lineNo2) throws Exception {
		List<ElementMetadata> argList = Utils.annotatedPair2List(rest);
		
		if (argList.size() != 2)
			throw new CompileError("Set requires exactly two arguments, got " + argList.size());
		
		bc.addAload(0); st++; // load this
		
		markLine(bc.currentPc(), argList.get(0).lineNo);
		int id = constants.add(argList.get(0).element);
		bc.addGetstatic(cls, "constants", "[Ledu/upol/paradise/compiler/types/ParadiseObject;");++st;
		bc.addIconst(id);							++st;						// load const
		bc.addOpcode(Opcode.AALOAD);				--st;						// load from array
		bc.addCheckcast("edu/upol/paradise/compiler/types/ParadiseSymbol");
		
		compileExpression(argList.get(1).element, true, argList.get(1).lineNo); 
		
		bc.addInvokevirtual(CompilerConstants.PARADISE_COMPILED_BLOCK_ST_SL, "set", 
				"(Ledu/upol/paradise/compiler/types/ParadiseSymbol;Ledu/upol/paradise/compiler/types/ParadiseObject;)V"); --st; --st; --st;
		
		bc.addGetstatic(CompilerConstants.NIL, "NIL", CompilerConstants.TYPE_NIL);	++st;
		
	}

	private void compileError(ParadisePair rest, boolean last, int lineNo2) throws Exception {
		List<ElementMetadata> argList = Utils.annotatedPair2List(rest);
		
		if (argList.size() != 1)
			throw new CompileError("Error requires exactly one argument, got " + argList.size());
		
		bc.addAload(0); st++; // load this
		
		markLine(bc.currentPc(), argList.get(0).lineNo);
		int id = constants.add(argList.get(0).element);
		bc.addGetstatic(cls, "constants", "[Ledu/upol/paradise/compiler/types/ParadiseObject;");++st;
		bc.addIconst(id);							++st;						// load const
		bc.addOpcode(Opcode.AALOAD);				--st;						// load from array
		bc.addCheckcast("edu/upol/paradise/compiler/types/ParadiseString");
		
		bc.addInvokevirtual(CompilerConstants.PARADISE_COMPILED_BLOCK_ST_SL, "setError", 
				"(Ledu/upol/paradise/compiler/types/ParadiseString;)V"); --st; --st;
	
		
		
		bc.addGetstatic(CompilerConstants.NIL, "NIL", CompilerConstants.TYPE_NIL);	++st;
	}

	private void compileEquals(ParadisePair rest, boolean last, int lineNo2) throws Exception {
		List<ElementMetadata> argList = Utils.annotatedPair2List(rest);
		
		if (argList.size() != 2)
			throw new CompileError("Equals requires exactly two arguments, got " + argList.size());
		
		compileExpression(argList.get(0).element, true, argList.get(0).lineNo); 
		compileExpression(argList.get(1).element, true, argList.get(1).lineNo);
		bc.addInvokeinterface("edu/upol/paradise/compiler/types/ParadiseObject", 
				"valueEquals", "(Ledu/upol/paradise/compiler/types/ParadiseObject;)Z", 2); st--;
		
		bc.add(Opcode.IFEQ); --st;
		int branchbyteCode = bc.currentPc();
		bc.add(0, 0);
		
		bc.addGetstatic(CompilerConstants.SYMBOL, "T", CompilerConstants.TYPE_SYMBOL);	++st;
		bc.add(Opcode.GOTO);
		int jmpbyteCode = bc.currentPc();
		bc.add(0, 0);
		
		bc.write16bit(branchbyteCode, bc.currentPc()-branchbyteCode+1);
		bc.addGetstatic(CompilerConstants.NIL, "NIL", CompilerConstants.TYPE_NIL);	++st;
		
		bc.write16bit(jmpbyteCode, bc.currentPc()-jmpbyteCode+1);
		bc.add(Opcode.NOP);
	}

	private void compileEQ(ParadisePair rest, boolean last, int lineNo2) throws Exception {
		List<ElementMetadata> argList = Utils.annotatedPair2List(rest);
		
		if (argList.size() != 2)
			throw new CompileError("Eq requires exactly two arguments, got " + argList.size());
		
		compileExpression(argList.get(0).element, true, argList.get(0).lineNo);
		compileExpression(argList.get(1).element, true, argList.get(1).lineNo);
		
		
		bc.add(Opcode.IF_ACMPEQ); --st;--st;
		int branchbyteCode = bc.currentPc();
		bc.add(0, 0);
		bc.addGetstatic(CompilerConstants.NIL, "NIL", CompilerConstants.TYPE_NIL);	++st;
		
		bc.add(Opcode.GOTO);
		int jmpbyteCode = bc.currentPc();
		bc.add(0, 0);
		
		bc.write16bit(branchbyteCode, bc.currentPc()-branchbyteCode+1);
		bc.addGetstatic(CompilerConstants.SYMBOL, "T", CompilerConstants.TYPE_SYMBOL);	++st;
		
		bc.write16bit(jmpbyteCode, bc.currentPc()-jmpbyteCode+1);
		bc.add(Opcode.NOP);
	}

	private void compileDefine(ParadisePair rest, boolean last, int lineNo2) throws Exception {
		List<ElementMetadata> argList = Utils.annotatedPair2List(rest);
		
		if (argList.size() != 2)
			throw new CompileError("Define requires exactly two argument, got " + argList.size());
		
		if (!(argList.get(0).element instanceof ParadiseSymbol))
			throw new CompileError("First argument to define must be a symbol.");
		
		addAuthoritativeCheck("!define");
		
		bc.addAload(0); st++; // load this
		
		markLine(bc.currentPc(), argList.get(0).lineNo);
		int id = constants.add(argList.get(0).element);
		bc.addGetstatic(cls, "constants", "[Ledu/upol/paradise/compiler/types/ParadiseObject;");++st;
		bc.addIconst(id);							++st;						// load const
		bc.addOpcode(Opcode.AALOAD);				--st;						// load from array
		bc.addCheckcast("edu/upol/paradise/compiler/types/ParadiseSymbol");
		
		compileExpression(argList.get(1).element, true, argList.get(1).lineNo); 
		
		bc.addInvokevirtual(CompilerConstants.PARADISE_COMPILED_BLOCK_ST_SL, "define", 
				"(Ledu/upol/paradise/compiler/types/ParadiseSymbol;Ledu/upol/paradise/compiler/types/ParadiseObject;)V"); --st; --st; --st;
		
		bc.addGetstatic(CompilerConstants.NIL, "NIL", CompilerConstants.TYPE_NIL);	++st;
	}

	private void compileIf(ParadisePair rest, boolean last, int lineNo) throws Exception {
		List<ElementMetadata> argList = Utils.annotatedPair2List(rest);
		
		if (argList.size() != 2 && argList.size() != 3)
			throw new CompileError("If requires exactly one or two arguments, got " + argList.size());
	
		bc.addGetstatic(CompilerConstants.NIL, "NIL", CompilerConstants.TYPE_NIL);++st;
		compileExpression(argList.get(0).element, true, argList.get(0).lineNo);
		bc.add(Opcode.IF_ACMPEQ); --st;--st;
		int branchbyteCode = bc.currentPc();
		bc.add(0, 0);
		
		compileExpression(argList.get(1).element, true, argList.get(0).lineNo);
		bc.add(Opcode.GOTO);
		int jmpbyteCode = bc.currentPc();
		bc.add(0, 0);
		
		bc.write16bit(branchbyteCode, bc.currentPc()-branchbyteCode+1);
		if (argList.size() == 2){
			markLine(bc.currentPc(), lineNo);
			bc.addGetstatic(CompilerConstants.NIL, "NIL", CompilerConstants.TYPE_NIL);	++st;
		} else 
			compileExpression(argList.get(2).element, true, argList.get(2).lineNo);
		
		bc.write16bit(jmpbyteCode, bc.currentPc()-jmpbyteCode+1);
		bc.add(Opcode.NOP);
	}

	private void compileProgn(ParadisePair rest, boolean last) throws Exception {
		List<ElementMetadata> argList = Utils.annotatedPair2List(rest);
		
		bc.add(Opcode.NOP); // For correct line management due to progn being no syntax element
		
		if (argList.size() == 0){
			// return NIL if there is nothing to do...
			if (rest instanceof AnnotatedCons)
				markLine(bc.currentPc(), 0);
				
			bc.addGetstatic(CompilerConstants.NIL, "NIL", CompilerConstants.TYPE_NIL);++st;
		} else 
			for (int i=0; i<argList.size(); i++){
				boolean elast = i == argList.size() - 1;
				ParadiseObject o = argList.get(i).element;
				compileExpression(o, elast, argList.get(i).lineNo);
			}
		
	}

	private void compileAtom(ParadiseObject o, boolean last, int lineNo) throws Exception {
		if (o instanceof ParadiseSymbol){
			compileSymbolLookup(o, lineNo);
		} else {
			markLine(bc.currentPc(), lineNo);
			int id = constants.add(o);
			bc.addGetstatic(cls, "constants", "[Ledu/upol/paradise/compiler/types/ParadiseObject;");++st;
			bc.addIconst(id);							++st;						// load const
			bc.addOpcode(Opcode.AALOAD);				--st;						// load from array
		}
	}

	private void compileQuote(ParadisePair rest, boolean last) throws Exception {
		List<ElementMetadata> argList = Utils.annotatedPair2List(rest);
		if (argList.size() != 1)
			throw new CompileError("Quote requires exactly one argument, got " + argList.size());
		
		ParadiseObject oo = argList.get(0).element;
		markLine(bc.currentPc(), argList.get(0).lineNo);
		int id = constants.add(oo);
		bc.addGetstatic(cls, "constants", "[Ledu/upol/paradise/compiler/types/ParadiseObject;");++st;
		bc.addIconst(id);							++st;							// load constant id
		bc.addOpcode(Opcode.AALOAD);				--st;							// load from array
	}

	private void compilePrint(ParadisePair rest, String callId, boolean last) throws Exception {
		List<ElementMetadata> argList = Utils.annotatedPair2List(rest);
		if (argList.size() != 1)
			throw new CompileError("!print requires exactly one argument, got " + argList.size());
		
		addAuthoritativeCheck(callId.equals("print") ? "!print" : "!println");
		
		ParadiseObject oo = argList.get(0).element;
		bc.addGetstatic("java/lang/System", "out", "Ljava/io/PrintStream;");	++st;
		compileExpression(oo, true, argList.get(0).lineNo);		
		markLine(bc.currentPc(), lineNo.peek());
		// Duplicate top because we need it later after print, if we need it
		bc.add(Opcode.DUP_X1);				++st;								// duplicate and insert two before
		bc.addInvokevirtual("java/io/PrintStream", callId, "(Ljava/lang/Object;)V"); --st; --st;

	}

	private void compileSymbolLookup(ParadiseObject oo, int line) throws Exception {
		usedSymbols.add(oo.toString()); // mark symbol as used
		
		lineNo.add(line);
		markLine(bc.currentPc(), lineNo.peek());
		int id = constants.add(oo);
		
		bc.addAload(0); 						++st;									// load this
		bc.addGetstatic(cls, "constants", "[Ledu/upol/paradise/compiler/types/ParadiseObject;");++st;
		bc.addIconst(id);						++st;									// load const
		bc.addOpcode(Opcode.AALOAD);			--st;									// load from array
		bc.addCheckcast("edu/upol/paradise/compiler/types/ParadiseSymbol");			// cast obj to symbol
		bc.addInvokevirtual(CompilerConstants.PARADISE_COMPILED_BLOCK_ST_SL, "findSymbolValue", 
				"(Ledu/upol/paradise/compiler/types/ParadiseSymbol;)Ledu/upol/paradise/compiler/types/ParadiseObject;"); --st;
		
				
		lineNo.pop();
	}
	
	private void addAuthoritativeCheck(String keyword) throws Exception {
		bc.addAload(0); st++; // load this
		bc.addGetfield("edu/upol/paradise/compiler/ParadiseCompiledBlockStub", "runtime", 
				"Ledu/upol/paradise/runtime/ParadiseRuntime;");
		bc.addInvokevirtual("edu.upol.paradise.runtime.ParadiseRuntime", "isAuthoritative", "()Z");
		bc.add(Opcode.IFNE); st--;
		int branchPos = bc.currentPc();
		bc.add(0, 0);
		bc.addNew("edu/upol/paradise/runtime/RuntimeExceptions$IllegalOperationException"); st++;
		bc.add(Opcode.DUP); st++;
		bc.addLdc(cacheStrings("Runtime is not authoritative for this function: " + keyword)); st++; 
		bc.addInvokespecial("edu/upol/paradise/runtime/RuntimeExceptions$IllegalOperationException", 
				"<init>", "(Ljava/lang/String;)V"); st--; st--;
		bc.add(Opcode.ATHROW); st--;
		bc.write16bit(branchPos, bc.currentPc()-(branchPos-1));
		bc.add(Opcode.NOP);
	}
	
	
	private int cacheStrings(String string) {
		if (!cache.containsKey(string)){
			cache.put(string, pool.addStringInfo(string));
		}
		return cache.get(string);
	}

	private void markLine(int pc, int line) throws Exception{
		markLine(pc, line, false);
	}
	
	private void markLine(int pc, int line, boolean override) throws Exception {
		if (line == lastLineWritten && !override)
			return;
		lineNumberStream.writeShort(pc);
		lineNumberStream.writeShort(line);
		lastLineWritten = line;
	}
}
